React Style Transitions from Scratch

Custom Hook

lib/useStyleTransition.tsx

import { useEffect, useState } from "react"

const STATE = {
  ENTERING: 'entering',
  ENTERED: 'entered',
  EXITING: 'exiting',
  EXITED: 'exit',
}

function useTransitionState(duration = 1000) {

  const [state, setState] = useState<string>()

  useEffect(() => {
    let timerId: any

    if (state === STATE.ENTERING) {
      timerId = setTimeout(() => setState(STATE.ENTERED), duration)
    } else if (state === STATE.EXITING) {
      timerId = setTimeout(() => setState(STATE.EXITED), duration)
    }

    return () => timerId && clearTimeout(timerId)

  }, [state, setState])


  return [state, setState]
}

export function useStyleTransitionControl(duration: number) {
  const [state, setState] = useTransitionState(duration)

  const enter = () => {
    if (state !== STATE.EXITING) {
      setState(STATE.ENTERING)
    }
  }

  const exit = () => {
    if (state !== STATE.ENTERING) {
      setState(STATE.EXITING)
    }
  }

  return [state, enter, exit]
}

Rendered Component

EmojiFade.tsx

import { useStyleTransitionControl } from "../../lib/useStyleTransition"
import { useState } from "react"

const defaultStyle = {
  transition: 'opacity 3000ms ease-in-out',
  opacity: 0,
}

const transitionStyles = {
  entering: { opacity: 1 },
  entered: { opacity: 1 },
  exiting: { opacity: 0 },
  exited: { opacity: 0 },
}

export function EmojiFade() {

  const [state, enter, exit] = useStyleTransitionControl()
  const [isOpen, setIsOpen] = useState(false)

  const style = {
    ...defaultStyle,
    ...transitionStyles[state] ?? {}
  }

  return (
    <StyledEmoji open={isOpen}>
      State: {state}

      <div style={style} > 
        <span role="img" aria-label="fading emoji"> 🐸 </span>
      </div>

      <button onClick={enter}>
        Enter
      </button>

      <button onClick={exit}>
        Exit
      </button>

    </StyledEmoji>
  )
}



Credits